#!/usr/bin/env node
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright 2019 Joyent, Inc.
 */

/*
 * bashstyle: check bash scripts for adherence to style guidelines, including:
 *
 *    o no lines longer than 80 characters
 *    o file does not end with a blank line
 *    o Do not use 'local' and var initialization *using a subshell* in the
 *      same statement. See
 *      <http://www.tldp.org/LDP/abs/html/localvar.html#EXITVALANOMALY01>
 *      for why not. Arguably this belongs in a separate 'bashlint'.
 *
 * Future enhancements could include:
 *    o indents consistent with respect to tabs, spaces
 *    o indents consistently sized (all are some multiple of the smallest
 *      indent, which must be a tab or 4 or 8 spaces)
 */

// jsl: ignore
'use strict';
// jsl: end

// eslint-disable-next-line
var VERSION = '2.0.0';

var mod_assert = require('assert');
var mod_fs = require('fs');

var nerrors = 0;

main();
process.exit(0);

function main()
{
	var files = process.argv.slice(2);

	if (files.length === 0) {
		console.error('usage: %s file1 [...]',
		    process.argv.slice(0, 2).join(' '));
		process.exit(2);
	}

	files.forEach(checkFile);

	if (nerrors != 0)
		process.exit(1);
}

function checkFile(filename)
{
	var text = mod_fs.readFileSync(filename, 'utf-8');
	var lines = text.split('\n');
	var i;
	var styled = false;
	var styleStart;

	mod_assert.ok(lines.length > 0);

	/*
	 * Expand tabs in each line and check for long lines.
	 */
	for (i = 1; i <= lines.length; i++) {
		var line = expandTabs(lines[i - 1]);

		if (i > 1 && lines[i-2].match(/# BASHSTYLED/)) {
			continue;
		}

		if (line.match(/# BEGIN BASHSTYLED/)) {
			styleStart = i;
			styled = true;
		}

		if (line.match(/# END BASHSTYLED/)) {
			if (styled != true) {
				nerrors++;
				console.log('%s: %d: END BASHSTYLED ' +
				    'w/o corresponding BEGIN', filename, i);
			}
			styled = false;
		}

		/*JSSTYLED*/
		if (!styled && line.match(/^\s*local\s+(\w+)\s*=.*\$\(/)) {
			nerrors++;
			/*JSSTYLED*/
			var m = line.match(/^\s*local\s+(\w+)\s*=/);
			console.log('%s: %d: declaring and setting a "local" ' +
				'var in the same statement ' +
				'ignores a subshell return code ' +
				'<http://www.tldp.org/LDP/abs/html/' +
				'localvar.html#EXITVALANOMALY01>: ' +
				'local %s=...',
				filename, i, m[1]);
		}

		// Regexplanation: non-[, [, space (contents) space, ], non-]
		// groups before and after brackets to ease search/replace.
		if (!styled && line.match(/(^|[^\[])\[(\s.+\s)\]([^\]])/)) {
			nerrors++;
			console.log('%s: %d: prefer [[ to [ for tests.',
			    filename, i);
		}

		if (!styled && line.length > 80) {
			nerrors++;
			console.log('%s: %d: line exceeds 80 columns',
			    filename, i);
		}

		if (!styled && line.match(/\s+$/)) {
			nerrors++;
			console.log('%s: %d: line ends in whitespace',
			    filename, i);
		}
	}

	if (styled) {
		nerrors++;
		console.log('%s: %d: BEGIN BASHSTYLED that does not END',
		            filename, styleStart);
	}


	/*
	 * No sane editor lets you save a file without a newline at the
	 * very end.
	 */
	if (lines[lines.length - 1].length !== 0) {
		nerrors++;
		console.log('%s: %d: file does not end with newline',
			filename, lines.length);
	}

	/*
	 * Since the file will always end with a newline, the last entry of
	 * "lines" will actually be blank.
	 */
	if (lines.length > 1 && lines[lines.length - 2].length === 0) {
		nerrors++;
		console.log('%s: %d: file ends with a blank line',
		    filename, lines.length - 1);
	}
}

function expandTabs(text)
{
	var out = '';
	var col = 0;
	var j, k;

	for (j = 0; j < text.length; j++) {
		if (text[j] != '\t') {
			out += text[j];
			col++;
			continue;
		}

		k = 8 - (col % 8);
		col += k;

		do {
			out += ' ';
		} while (--k > 0);

		col += k;
	}

	return (out);
}
